/*
 * Copyright 2011 Clean GWT
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package com.cleangwt.validation.client;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.i18n.client.DateTimeFormat.PredefinedFormat;
import com.google.gwt.user.client.ui.HasValue;
import com.google.gwt.user.client.ui.Widget;

/**
 * The client validator for verifying value of components.
 * 
 * @author Jess
 * @version 1.0
 * @since 1.0
 */
public class Validator {

  private static DateTimeFormat datetimeFormat;

  /**
   * Sets the message format.
   * 
   * @param datetimeFormat the date time format
   */
  public static void setDateFormat(DateTimeFormat datetimeFormat) {
    Validator.datetimeFormat = datetimeFormat;
  }

  /**
   * Gets the date time format for message formating.
   * 
   * @return the date time format for message formating
   */
  static DateTimeFormat getDatetimeFormat() {
    if (datetimeFormat == null) {
      datetimeFormat = DateTimeFormat.getFormat(PredefinedFormat.YEAR_MONTH_NUM_DAY);
    }
    return datetimeFormat;
  }

  private List<ValidationContext> contexts = new ArrayList<ValidationContext>();
  private List<ValidationContext> errorContexts;
  /**
   * the default context for validatiosn without binding inputs
   */
  private ValidationContext defaultContext;
  private ValidationErrorHandler violdationHandler;
  private boolean reportAsSingleError;
  private String[] lastValidatedGroup;
  private boolean activated;

  private List<Validator> validstors;

  /**
   * Creates a validator.
   * 
   */
  public Validator() {
    this(new DefaultErrorHandler());
  }

  /**
   * Creates a validator with the given 'errorHandler'.
   * 
   * @param errorHandler the {@link ValidationErrorHandler}
   */
  public Validator(ValidationErrorHandler errorHandler) {
    this(errorHandler, true);
  }

  /**
   * Creates a validator with the given 'errorHandler' and 'reportAsSingleError'.
   * 
   * @param errorHandler the {@link ValidationErrorHandler}
   * @param reportAsSingleViolation whether to report simple error
   */
  public Validator(ValidationErrorHandler errorHandler, boolean reportAsSingleError) {
    this.violdationHandler = errorHandler;
    this.reportAsSingleError = reportAsSingleError;
  }

  /**
   * Adds a sub validator.
   * 
   * @param validator the validator
   */
  public void addValidator(Validator validator) {
    assert validator != this;
    if (validstors == null) {
      validstors = new ArrayList<Validator>();
    }
    validstors.add(validator);
  }

  /**
   * Adds a {@link ValidationContext} for adding validations with the given inputs.
   * 
   * @param inputs the inputs to bind
   * @return the {@link ValidationContext}
   */
  public ValidationContext addContext(HasValue<?>... inputs) {
    assert !activated : "The validator has been activated.";
    assert inputs != null && inputs.length > 0 : "For adding context without input, call #addValidaiton() instead.";
    ValidationContext context = new ValidationContext(inputs);
    contexts.add(context);
    return context;
  }

  /**
   * Adds a validation to the default context.
   * 
   * @param validation the validaiton
   */
  public void addValidation(Validation validation) {
    assert !activated : "The validator has been activated.";
    if (this.defaultContext == null) {
      this.defaultContext = new ValidationContext(null);
    }
    defaultContext.add(validation);
  }

  /**
   * Builds the {@link MultiFieldBuilder} with the given inputs.
   * 
   * @param inputs the inputs
   */
  public MultiFieldBuilder build(HasValue<?>... inputs) {
    assert !activated : "The validator has been activated.";
    MultiFieldBuilder builder = new MultiFieldBuilder(inputs);
    build(builder);
    return builder;
  }

  /**
   * Builds the {@link SingleFieldBuilder} with the given input.
   * 
   * @param input the input
   */
  public SingleFieldBuilder build(HasValue<?> input) {
    assert !activated : "The validator has been activated.";
    SingleFieldBuilder builder = new SingleFieldBuilder(input);
    build(builder);
    return builder;
  }

  /**
   * Builds the {@link ValidationContextBuilder}.
   * 
   * @param builder the validation context builder
   */
  public void build(ValidationContextBuilder builder) {
    assert !activated : "The validator has been activated.";
    contexts.add(builder.getContext());
  }

  /**
   * Resets this validator.
   */
  public void reset() {
    assert activated : "This validator has not been activated";
    errorContexts.clear();
    for (ValidationContext context : contexts) {
      context.reset();
      this.resetComponentError(context);
    }
    this.lastValidatedGroup = null;
  }

  /**
   * Validates the default groups.
   * 
   * @return true for valid, false for invalid
   */
  public boolean validate() {
    return validate((String[]) null);
  }

  /**
   * Validates the specified groups.
   * 
   * @return true for valid, false for invalid
   */
  public boolean validate(String... groups) {
    if (!this.activated) {
      this.activate();
    }
    boolean groupChanged = !Arrays.equals(lastValidatedGroup, groups);

    if (groupChanged) {
      this.reset();
    } else {
      // just to clear errorContexts and re check or re add error contexts
      // to it for keeping orders.
      this.errorContexts.clear();
    }

    this.lastValidatedGroup = groups;

    // checks each contexts
    for (ValidationContext ctx : contexts) {
      // context without binding any inputs should be always re validated
      if (groupChanged || !ctx.isValidated() || !ctx.hasInput()) {
        // if context has not been validated or requires revalidating,
        // validate it
        String violation = ctx.validate(this, groups);
        // if the given groups is not been validated, do not update
        // context
        if (ctx.isValidated()) {
          this.updateContextError(ctx, violation);
        }
      } else {
        // if context has been validated and has an error, re add it
        if (ctx.getErrorMessage() != null) {
          errorContexts.add(ctx);
        }
      }

    }

    List<ValidaitonError> violations = this.getValidationErrors(reportAsSingleError);

    if (validstors != null) {
      for (Validator v : validstors) {
        v.validate(groups);
        if (violations.isEmpty() || !reportAsSingleError) {
          violations.addAll(v.getValidationErrors(reportAsSingleError));
        }
      }

    }

    if (!violations.isEmpty()) {
      violdationHandler.onValidationError(violations);
      return false;
    }

    return true;
  }

  /**
   * Gets the validation errors.
   * 
   * @param reportAsSingleError whether to report as single error
   * @return the validation erros
   */
  public List<ValidaitonError> getValidationErrors(final boolean reportAsSingleError) {
    assert activated : "This validator has not been activated";
    List<ValidaitonError> errors = new ArrayList<ValidaitonError>();
    for (ValidationContext ctx : errorContexts) {
      Widget invalidField = null;
      if (ctx.hasInput()) {
        invalidField = (Widget) ctx.getInputs()[0];
      }
      ValidaitonError err = new ValidaitonError(invalidField, ctx.getErrorMessage());
      errors.add(err);
      if (reportAsSingleError) {
        return errors;
      }
    }
    return errors;
  }

  /**
   * Finds error context for the specified input and just to skip the given context.
   * 
   * @param context the context to be ignored
   * @param input the input has error
   * @return the context contains the error input
   */
  private ValidationContext findErrorContext(ValidationContext context, HasValue<?> input) {
    for (ValidationContext ctx : errorContexts) {
      if (context != ctx && ctx.hasInput(input)) {
        return ctx;
      }
    }
    return null;
  }

  /**
   * Resets the component error for the specified validation context.
   * 
   * @param context the validation context
   */
  private void resetComponentError(ValidationContext context) {
    if (context.hasInput()) {
      for (HasValue<?> input : context.getInputs()) {
        // if is error in other context, do not clear the error message
        ValidationContext ctx = this.findErrorContext(context, input);
        if (ctx == null) {
          violdationHandler.resetCompoentError((Widget) input);
        } else {
          violdationHandler.setComponentError((Widget) input, ctx.getErrorMessage());
        }
      }
    }
  }

  /**
   * Sets the component error for the specified validation context.
   * 
   * @param context the validation context
   * @param errorMessage the error message
   */
  private void setComponentError(ValidationContext context, String errorMessage) {
    assert errorMessage != null;
    if (context.hasInput()) {
      if (context.getInputs().length == 1) {
        // the error message of single field context is prior to else,
        // so update the error without check
        violdationHandler.setComponentError((Widget) context.getInputs()[0], errorMessage);
      } else {
        // for error of multi field context, it should not override any
        // exist error in other context
        for (HasValue<?> input : context.getInputs()) {
          ValidationContext ctx = findErrorContext(context, input);
          if (ctx == null) {
            violdationHandler.setComponentError((Widget) input, errorMessage);
          } else {
            violdationHandler.setComponentError((Widget) input, ctx.getErrorMessage());
          }

        }
      }
    }
  }

  /**
   * Checks the error message for the specified validation context and updates component errors for
   * it.
   * 
   * @param context the validation context
   */
  void updateContextError(ValidationContext context, String errorMessage) {
    context.setErrorMessage(errorMessage);
    if (errorMessage == null) {
      errorContexts.remove(context);
      resetComponentError(context);
    } else {
      if (!errorContexts.contains(context)) {
        errorContexts.add(context);
      }
      setComponentError(context, errorMessage);
    }
  }

  @SuppressWarnings({"unchecked", "rawtypes"})
  public void activate() {
    assert !activated : "The validator has been activated.";
    activated = true;
    errorContexts = new ArrayList<ValidationContext>();
    Set<HasValue<?>> inputs = new HashSet<HasValue<?>>();
    for (ValidationContext ctx : this.contexts) {
      // ctx.activate();
      for (final HasValue<?> input : ctx.getInputs()) {
        if (!inputs.contains(input)) {
          input.addValueChangeHandler(new ValueChangeHandler() {

            @Override
            public void onValueChange(ValueChangeEvent event) {
              validateField(input);
            }
          });
        }
      }
    }
    if (defaultContext != null) {
      this.contexts.add(defaultContext);
    }
  }

  private void validateField(HasValue<?> input) {
    List<ValidationContext> contexts = new ArrayList<ValidationContext>();
    // find relative contexts
    for (ValidationContext ctx : this.contexts) {
      if (ctx.hasInput(input)) {
        contexts.add(ctx);
      }
    }
    // validate each context for the field
    for (ValidationContext ctx : contexts) {
      this.updateContextError(ctx, ctx.validate(this, null));
    }
  }

}
